昨天已經了解Reconciliation是什麼樣的過程,以及Reconciliationy的過程中是怎麼比較新舊Virtual DOM,今天讓我們從一個判斷有沒有進行畫面更新的例子來繼續了解Reconciliation的部分。
今天一開始先來分享一個大家可能會或已經踩到過的雷,會發生這樣的雷主要原因就是與Reconciliation有關聯。
這個畫面中有一個切換按鈕,可以切換不同的form。
function App() {
const [isProfileForm, setIsProfileForm] = useState(true);
const toggle = () => {
setIsProfileForm(!isProfileForm)
};
return (
<div className="App">
<div className="container">
<h1>{isProfileForm ? 'Profile' : 'Account Info' }</h1>
{
isProfileForm ? (
<form>
<input placeholder="first name"/>
<input placeholder="last name"/>
<input placeholder="address"/>
</form>
) : (
<form>
<input placeholder="account name" />
<input placeholder="line ID" />
</form>
)
}
</div>
<button onClick={toggle}>Change Form Type</button>
</div>
);
}
可以先思考一下當我們在input透過輸入一個欄位後,在按下切換按鈕會發生什麼事情。第一直覺應該都會覺得原本在input輸入的內容會消失,因為當我們透過按鈕切換form的類型時,就會把整組的form替換掉,尤其是下述這段程式碼就是在進行form替換的部分。
isProfileForm ? (
<form>
<input placeholder="first name"/>
<input placeholder="last name"/>
<input placeholder="address"/>
</form>
) : (
<form>
<input placeholder="account name" />
<input placeholder="line ID" />
</form>
)
這樣的程式碼內容的確是我們理解並且想實作出來的邏輯,但是在Reconciliation過程中比較新舊Virtual DOM的時候,也認為整個form包含input都被替換掉了嗎?
先來看看實際上的結果是否真的如預期。
什麼!不是換了整組form了嗎?怎麼輸入的值還停留在input上呢?
這就需要回來思考一下「在Reconciliation過程中比較新舊Virtual DOM的時候,是否也認為form包含input被替換了?」。昨天在看「如何判斷新舊Virtual DOM有無不同」的時候,有提到主要是會先比較HTML元素有無不同,再來比對屬性,如果HTML元素一樣,只有屬性不同,就只會跟新屬性的部分。說到這裡,應該稍微有些靈感了。沒錯!雖然我們透過isProfileForm替換了整組的form,但是在往下比對後,不只form沒有變動,children也依然還是input,不同的地方只有placeholder這個屬性
,所以只會更新屬性,並在最下面插入一個input,而不會把整個input都移除掉再重新建立新的input。
從這裡就可以看到當切換form的類型時,只有placeholder和最下方插入的input會閃動一下,其他則維持不變。
如果想要讓input的內容清空,就需要用一些方式讓React在進行Reconciliation時,能夠知道現在已經替換成別的input。而這個時候就可以使用key
來讓每個input都有一個獨特的身分,進而讓React可以辨識這些input雖然都是input,但其實是不同的input。
當我們在input上補上一個獨一無二的key後,在比對新舊元素有無不同時,React就可以知道雖然type都是input,但是它們卻是不同key的input,所以不能只更新placeholder屬性,還必須移除掉現在原本的input,重新創建input。
{
isProfileForm ? (
<form>
<input key="first-name" placeholder="first name"/>
<input key="last-name" placeholder="last name"/>
<input key="address" placeholder="address"/>
</form>
) : (
<form>
<input key="account-name" placeholder="account name" />
<input key="line-id" placeholder="line ID" />
</form>
)
}
透過上面的實作結果就可以看到加上key後,當替換form type的時候,所有input就都會被替換掉,原本輸入的文字也就不會被保存在那裡。
當我們給元素補上一個key之後,除了可以讓React辨識這些元素的身分,進而分辨出現在有沒有變成不同key的元素,還有一個從辨識身分延伸出來的功效,就是讓React在Reconciliation的過程中,知道某些元素原本就已經存在過,也就知道不需要再重新創建並更新這部分的DOM。
還記得昨天有提到一個,把名字加在最前面後,就算後面的child都是一樣的,還是全部都被移除,重新創建的例子嗎?那個例子也就是可以透過加上key來優化的情境。
// 插入新名字前
<div>
<p key="Jolin">Jolin</p>
<p key="Hebe">Hebe</p>
</div>
// 插入新名字後
<div>
<p key="IU">IU</p>
<p key="Jolin">Jolin</p>
<p key="Hebe">Hebe</p>
</div>
這時候的新的Virtual DOM及舊的Virtual DOM會是類似這樣的物件。
// 改動前
{
type: 'div',
props: {},
children: [
{
type: 'p',
props: { key: 'Jolin' },
children: 'Jolin'
},
{
type: 'p',
props: { key: 'Hebe' },
children: 'Hebe'
}
]
}
// 改動後
{
type: 'div',
props: {},
children: [
{
type: 'p',
props: { key: 'IU' },
children: 'IU'
},
{
type: 'p',
props: { key: 'Jolin' },
children: 'Jolin'
},
{
type: 'p',
props: { key: 'Hebe' },
children: 'Hebe'
}
]
}
加上key後,React就可以確實地知道哪些child是原本就存在的內容,所以在實作結果中也就能看到實際上只有真正新加入的那個p元素有閃了一下。
雖然加上key能達到讓React避免重新創建原本就一樣的元素,但並不是什麼值都可以被拿來幫作key使用,用在元素上的key必須要是一個唯一值。
為什麼呢?
前面我們已經透過例子了解到如果加上key,就可以讓React在進行Reconciliation中的比較環節時,清楚的知道哪些元素是原本就已經存在過的舊元素,也就是說React會把這個key當作身分證。既然key值是讓React辨識元素的身分的身分證,那這個key的值當然就必須是沒有重複且只限特定元素可以擁有的值,否則就會在順序有變動時,因為身分辨識的錯亂,而出現一些state錯亂的狀況。
有時候我們可能會因為方便使用index來當作這個key使用,雖然以index本身來說,的確是獨一無二的值,但是卻不是只有限定某元素才可以擁有的值,舉例來說只要擺放在陣列的第一個位置,那個陣列元素的index就是0,當排序有變動,這個元素的index可能就會變成1或其他數字,這樣的狀況也會導致前面提到的state的錯亂狀況。
實際上會出現什麼樣的情況,大家可以親自去玩玩看React官方文件中提供的範例,比較能有更深刻的感受。
延伸補充:如果對Vue比較熟悉的朋友,應該都對key的使用不陌生,因為使用v-for跑迴圈的時候,也會需要放上key。在Vue迴圈中,用index當作key來使用時,如果是排序會變動的元素,一樣會發生上述提到的類似的問題,所以官方一樣也是建議不用把inde當作key來使用。
到目前為止我們已經了解比較新舊Virtual DOM的不同之處是什麼樣的一個過程,以及什麼樣的狀況會造成相異處的出現,還有了解key的使用。這裡再從之前提到的例子,來看看Reconciliation對於效能會有怎麼樣的影響。
一般來說,Reconciliation主要是為了減少對真實DOM操作所產生的效能消耗,因為他可以在比較的過程中,找出真正需要更新的內容,然後只更新需要更新的部份。像是如果它比對過後,發現只有class屬性有變動,它就會只更新class屬性的部會,不會將整個元素都重新創建並更新上去。但是在某些特定的操作中,還是有可能會造成一些不必要的效能消耗。
在前面我們有看到的一個例子有出現只是插入一個新的p元素在前面,就造成後面的p元素也需要重新創建更新情況。前面的提到的例子,看起來雖然好像沒有什麼大影響,因為那個例子比較單純,但是如果裡面的children內還有內涵很多層children的話,這樣因為插入一個p元素,就需要把下面的一大堆內容都重新創建替換掉的話,就很有可能對效能有明顯的負面影響。不過幸好在這個情境下我們還是可以透過加上key來讓React去辨識相同元素間的特定身分,來達到功能和效能都兼顧的效果。不過如果沒有必要的話,也還是盡可能避免這樣的操作出現會比較好。
今天我們透過一個例子來看一些我們對於Reconciliation可能會出現誤解的地方,其中的重點就是Reconciliation比較差異的方法不是比較我們肉眼或是人腦認為的不同的地方,而是透過比較HTML元素、屬性、children的變動來判斷有無差異
。也從這個例子延伸了解key在Reconciliation過程中,主要是用作標示元素身份,讓React在Reconciliation的時候,可以知道看起來是相同HTML元素的內容,其實已經是不同身份的元素,還有使用key的時候需要注意不要使用非唯一值及不要使用index,避免產生一些資料混亂的問題。關於認識Reconciliation的部分就在這裡告一個段落了,明天會接著了解關於元件的一生-生命週期的部份。